Вийдіть за рамки базової типізації. Освоюйте розширені функції TypeScript, такі як умовні типи, шаблонні літерали та маніпулювання рядками, щоб створювати неймовірно надійні та безпечні за типом API. Всеосяжний посібник для глобальних розробників.
Розкриття Повного Потенціалу TypeScript: Глибоке Занурення в Умовні Типи, Шаблонні Літерали та Розширені Маніпуляції з Рядками
У світі сучасної розробки програмного забезпечення TypeScript еволюціонував далеко за межі своєї початкової ролі як простого перевірки типів для JavaScript. Він став складним інструментом для того, що можна описати як програмування на рівні типів. Ця парадигма дозволяє розробникам писати код, який оперує самими типами, створюючи динамічні, самодокументовані та надзвичайно безпечні API. В основі цієї революції лежать три потужні функції, що працюють у поєднанні: Умовні типи, Шаблонні літеральні типи та набір вбудованих типів для маніпулювання рядками.
Для розробників по всьому світу, які прагнуть підвищити свої навички TypeScript, розуміння цих концепцій більше не є розкішшю — це необхідність для створення масштабованих і підтримуваних програм. Цей посібник проведе вас у глибоке занурення, починаючи з фундаментальних принципів і переходячи до складних, реальних патернів, які демонструють їхню об’єднану силу. Незалежно від того, чи створюєте ви систему дизайну, безпечний за типом клієнт API або складну бібліотеку обробки даних, освоєння цих функцій кардинально змінить спосіб написання коду на TypeScript.
Основа: Умовні Типи (Тернарний оператор `extends`)
За своєю суттю, умовний тип дозволяє вибрати один із двох можливих типів на основі перевірки співвідношення типів. Якщо ви знайомі з тернарним оператором JavaScript (condition ? valueIfTrue : valueIfFalse), ви знайдете синтаксис одразу інтуїтивно зрозумілим:
type Result = SomeType extends OtherType ? TrueType : FalseType;
Тут ключове слово extends діє як наша умова. Воно перевіряє, чи SomeType можна призначити OtherType. Давайте розберемо це на простому прикладі.
Базовий Приклад: Перевірка Типу
Уявіть, що ми хочемо створити тип, який розв’язується в true, якщо даний тип T є рядком, і false в іншому випадку.
type IsString
Потім ми можемо використовувати цей тип наступним чином:
type A = IsString<"hello">; // type A is true
type B = IsString<123>; // type B is false
Це фундаментальний будівельний блок. Але справжня сила умовних типів розкривається в поєднанні з ключовим словом infer.
Сила `infer`: Вилучення Типів з Середини
Ключове слово infer змінює правила гри. Воно дозволяє оголосити нову змінну generic-типу всередині умови extends, ефективно захоплюючи частину типу, який ви перевіряєте. Уявіть це як оголошення змінної на рівні типу, яка отримує своє значення з зіставлення зі зразком.
Класичним прикладом є розгортання типу, що міститься в Promise.
type UnwrapPromise
Давайте проаналізуємо це:
T extends Promise: Це перевіряє, чиTєPromise. Якщо так, TypeScript намагається зіставити структуру.infer U: Якщо зіставлення успішне, TypeScript захоплює тип, до якого розв’язуєтьсяPromise, і поміщає його в нову змінну типу під назвоюU.? U : T: Якщо умова істинна (TбувPromise), результуючим типом єU(розгорнутий тип). В іншому випадку результуючим типом є просто початковий типT.
Використання:
type User = { id: number; name: string; };
type UserPromise = Promise
type UnwrappedUser = UnwrapPromise
type UnwrappedNumber = UnwrapPromise
Цей шаблон настільки поширений, що TypeScript включає вбудовані службові типи, такі як ReturnType, який реалізується з використанням того самого принципу для вилучення типу повернення функції.
Дистрибутивні Умовні Типи: Робота з Об'єднаннями
Захоплюючою та вирішальною поведінкою умовних типів є те, що вони стають дистрибутивними, коли тип, який перевіряється, є «голим» generic-параметром типу. Це означає, що якщо ви передаєте йому тип об’єднання, умова буде застосована до кожного члена об’єднання окремо, і результати будуть зібрані назад у нове об’єднання.
Розглянемо тип, який перетворює тип на масив цього типу:
type ToArray
Якщо ми передамо тип об’єднання до ToArray:
type StrOrNumArray = ToArray
Результатом є не (string | number)[]. Оскільки T є голим параметром типу, умова розподіляється:
ToArrayстаєstring[]ToArrayстаєnumber[]
Кінцевим результатом є об’єднання цих окремих результатів: string[] | number[].
Ця дистрибутивна властивість неймовірно корисна для фільтрації об’єднань. Наприклад, вбудований службовий тип Extract використовує це для вибору членів з об’єднання T, які можна призначити U.
Якщо вам потрібно запобігти цій дистрибутивній поведінці, ви можете обернути параметр типу в кортеж з обох боків умови extends:
type ToArrayNonDistributive
type StrOrNumArrayUnified = ToArrayNonDistributive
З цією міцною основою давайте дослідимо, як ми можемо конструювати динамічні рядкові типи.
Створення Динамічних Рядків на Рівні Типів: Шаблонні Літеральні Типи
Представлені в TypeScript 4.1, Шаблонні літеральні типи дозволяють визначати типи, які мають форму шаблонних літеральних рядків JavaScript. Вони дають змогу об’єднувати, комбінувати та генерувати нові рядкові літеральні типи з існуючих.
Синтаксис саме такий, як ви очікуєте:
type World = "World";
type Greeting = `Hello, ${World}!`; // type Greeting is "Hello, World!"
Це може здатися простим, але його сила полягає в поєднанні його з об’єднаннями та generics.
Об'єднання та Перестановки
Коли шаблонний літеральний тип включає об’єднання, він розширюється до нового об’єднання, що містить кожну можливу рядкову перестановку. Це потужний спосіб створити набір чітко визначених констант.
Уявіть, що визначаєте набір властивостей CSS для полів:
type Side = "top" | "right" | "bottom" | "left";
type MarginProperty = `margin-${Side}`;
Результуючий тип для MarginProperty є:
"margin-top" | "margin-right" | "margin-bottom" | "margin-left"
Це ідеально підходить для створення безпечних за типом властивостей компонента або аргументів функції, де дозволені лише певні формати рядків.
Поєднання з Generics
Шаблонні літерали справді сяють, коли використовуються з generics. Ви можете створювати типи фабрик, які генерують нові рядкові літеральні типи на основі деяких вхідних даних.
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
Цей шаблон є ключем до створення динамічних, безпечних за типом API. Але що, якщо нам потрібно змінити регістр рядка, наприклад, змінити "user" на "User", щоб отримати "onUserChange"? Ось тут і з’являються типи маніпулювання рядками.
Набір Інструментів: Вбудовані Типи для Маніпулювання Рядками
Щоб зробити шаблонні літерали ще потужнішими, TypeScript надає набір вбудованих типів для маніпулювання рядковими літералами. Це як службові функції, але для системи типів.
Модифікатори Регістру: `Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize`
Ці чотири типи роблять саме те, що підказують їхні назви:
Uppercase: Перетворює весь тип рядка у верхній регістр.type LOUD = Uppercase<"hello">; // "HELLO"Lowercase: Перетворює весь тип рядка в нижній регістр.type quiet = Lowercase<"WORLD">; // "world"Capitalize: Перетворює перший символ типу рядка у верхній регістр.type Proper = Capitalize<"john">; // "John"Uncapitalize: Перетворює перший символ типу рядка в нижній регістр.type variable = Uncapitalize<"PersonName">; // "personName"
Давайте переглянемо наш попередній приклад і покращимо його за допомогою Capitalize для створення звичайних імен обробників подій:
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
Тепер у нас є всі частини. Давайте подивимося, як вони поєднуються, щоб вирішити складні, реальні проблеми.
Синтез: Об'єднання Всіх Трьох для Розширених Патернів
Тут теорія зустрічається з практикою. Поєднуючи умовні типи, шаблонні літерали та маніпулювання рядками, ми можемо створити неймовірно складні та безпечні визначення типів.
Патерн 1: Повністю Безпечний за Типом Генератор Подій
Мета: Створити generic-клас EventEmitter з методами, такими як on(), off() і emit(), які є повністю безпечними за типом. Це означає:
- Ім’я події, передане в методи, має бути дійсною подією.
- Корисне навантаження, передане в
emit(), має відповідати типу, визначеному для цієї події. - Функція зворотного виклику, передана в
on(), має приймати правильний тип корисного навантаження для цієї події.
Спочатку ми визначаємо карту імен подій для їхніх типів корисного навантаження:
interface EventMap {
"user:created": { userId: number; name: string; };
"user:deleted": { userId: number; };
"product:added": { productId: string; price: number; };
}
Тепер ми можемо створити generic-клас EventEmitter. Ми використаємо generic-параметр Events, який має розширювати нашу структуру EventMap.
class TypedEventEmitter
private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
// Метод `on` використовує generic `K`, який є ключем нашої карти Events
on
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(callback);
}
// Метод `emit` гарантує, що корисне навантаження відповідає типу події
emit
this.listeners[event]?.forEach(callback => callback(payload));
}
}
Давайте створимо екземпляр і використаємо його:
const appEvents = new TypedEventEmitter
// Це безпечно за типом. Корисне навантаження правильно виводиться як { userId: number; name: string; }
appEvents.on("user:created", (payload) => {
console.log(`User created: ${payload.name} (ID: ${payload.userId})`);
});
// TypeScript видасть помилку тут, оскільки "user:updated" не є ключем в EventMap
// appEvents.on("user:updated", () => {}); // Error!
// TypeScript видасть помилку тут, оскільки в корисному навантаженні відсутня властивість "name"
// appEvents.emit("user:created", { userId: 123 }); // Error!
Цей патерн забезпечує безпеку під час компіляції для того, що традиційно є дуже динамічною та схильною до помилок частиною багатьох програм.
Патерн 2: Безпечний за Типом Доступ до Шляху для Вкладених Об'єктів
Мета: Створити службовий тип, PathValue, який може визначити тип значення у вкладеному об’єкті T, використовуючи рядковий шлях у точковій нотації P (наприклад, "user.address.city").
Це дуже розширений патерн, який демонструє рекурсивні умовні типи.
Ось реалізація, яку ми розберемо:
type PathValue
? Key extends keyof T
? PathValue
: never
: P extends keyof T
? T[P]
: never;
Давайте простежимо його логіку на прикладі: PathValue
- Початковий Виклик:
Pє"a.b.c". Це відповідає шаблонному літералу`${infer Key}.${infer Rest}`. Keyвиводиться як"a".Restвиводиться як"b.c".- Перша Рекурсія: Тип перевіряє, чи
"a"є ключемMyObject. Якщо так, він рекурсивно викликаєPathValue. - Друга Рекурсія: Тепер
Pє"b.c". Він знову відповідає шаблонному літералу. Keyвиводиться як"b".Restвиводиться як"c".- Тип перевіряє, чи
"b"є ключемMyObject["a"], і рекурсивно викликаєPathValue. - Базовий Випадок: Нарешті,
Pє"c". Це не відповідає`${infer Key}.${infer Rest}`. Логіка типу переходить до другої умови:P extends keyof T ? T[P] : never. - Тип перевіряє, чи
"c"є ключемMyObject["a"]["b"]. Якщо так, результатом єMyObject["a"]["b"]["c"]. Якщо ні, цеnever.
Використання за допомогою допоміжної функції:
declare function get
const myObject = {
user: {
name: "Alice",
address: {
city: "Wonderland",
zip: 12345
}
}
};
const city = get(myObject, "user.address.city"); // const city: string
const zip = get(myObject, "user.address.zip"); // const zip: number
const invalid = get(myObject, "user.email"); // const invalid: never
Цей потужний тип запобігає помилкам часу виконання через друкарські помилки в шляхах і забезпечує ідеальне виведення типів для глибоко вкладених структур даних, що є поширеною проблемою в глобальних програмах, які мають справу зі складними відповідями API.
Найкращі Практики та Міркування щодо Продуктивності
Як і з будь-яким потужним інструментом, важливо використовувати ці функції з розумом.
- Надавайте Пріоритет Читабельності: Складні типи можуть швидко стати нечитабельними. Розбивайте їх на менші, добре названі допоміжні типи. Використовуйте коментарі, щоб пояснити логіку, так само, як і зі складним кодом часу виконання.
- Розумійте Тип `never`: Тип
neverє вашим основним інструментом для обробки станів помилок і фільтрації об’єднань в умовних типах. Він представляє стан, який ніколи не повинен виникати. - Остерігайтеся Обмежень Рекурсії: TypeScript має обмеження глибини рекурсії для створення екземплярів типу. Якщо ваші типи занадто глибоко вкладені або нескінченно рекурсивні, компілятор видасть помилку. Переконайтеся, що ваші рекурсивні типи мають чіткий базовий випадок.
- Слідкуйте за Продуктивністю IDE: Надзвичайно складні типи іноді можуть впливати на продуктивність мовного сервера TypeScript, що призводить до повільнішого автозавершення та перевірки типів у вашому редакторі. Якщо ви відчуваєте сповільнення, перевірте, чи можна спростити або розбити складний тип.
- Знайте, Коли Зупинитися: Ці функції призначені для вирішення складних проблем безпеки типів і досвіду розробників. Не використовуйте їх для надмірного проектування простих типів. Мета полягає в покращенні ясності та безпеки, а не в додаванні зайвої складності.
Висновок
Умовні типи, шаблонні літерали та типи маніпулювання рядками — це не просто ізольовані функції; це тісно інтегрована система для виконання складної логіки на рівні типів. Вони дають нам змогу вийти за рамки простих анотацій і створювати системи, які глибоко усвідомлюють власну структуру та обмеження.
Освоївши це тріо, ви можете:
- Створювати Самодокументовані API: Самі типи стають документацією, направляючи розробників використовувати їх правильно.
- Усунути Цілі Класи Помилок: Помилки типу виявляються під час компіляції, а не користувачами у виробництві.
- Покращити Досвід Розробників: Насолоджуйтеся багатим автозавершенням і вбудованими повідомленнями про помилки навіть для найбільш динамічних частин вашої кодової бази.
Прийняття цих розширених можливостей перетворює TypeScript із мережі безпеки на потужного партнера в розробці. Це дозволяє вам закодувати складну бізнес-логіку та інваріанти безпосередньо в систему типів, гарантуючи, що ваші програми будуть більш надійними, підтримуваними та масштабованими для глобальної аудиторії.